using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using ComTypes = System.Runtime.InteropServices.ComTypes;
using System.IO;

namespace Tsanie.ShellExtension {

    #region - SteorageStream -

    /// <summary>
    /// A wrapper for the native IStream object.
    /// </summary>
    public class StorageStream : Stream, IDisposable {
        IStream _stream;
        private ComTypes.STATSTG _stats;
        private bool _isReadOnly = false;

        internal StorageStream(IStream stream, bool readOnly) {
            if (stream == null)
                throw new ArgumentNullException("stream");
            stream.Stat(out _stats, 0);
            _isReadOnly = readOnly;
            this._stream = stream;
        }

        /// <summary>
        /// Reads a single byte from the stream, moving the current position ahead by 1.
        /// </summary>
        /// <returns>A single byte from the stream, -1 if end of stream.</returns>
        public override int ReadByte() {
            ThrowIfDisposed();
            byte[] buffer = new byte[1];
            if (Read(buffer, 0, 1) > 0) { return buffer[0]; }
            return -1;
        }

        /// <summary>
        /// Writes a single byte to the stream
        /// </summary>
        /// <param name="value">Byte to write to stream</param>
        public override void WriteByte(byte value) {
            ThrowIfDisposed();
            byte[] buffer = new byte[] { value };
            Write(buffer, 0, 1);
        }

        /// <summary>
        /// Gets whether the stream can be read from.
        /// </summary>
        public override bool CanRead { get { return _stream != null; } }

        /// <summary>
        /// Gets whether seeking is supported by the stream.
        /// </summary>
        public override bool CanSeek { get { return _stream != null; } }

        /// <summary>
        /// Gets whether the stream can be written to.
        /// Always false.
        /// </summary>
        public override bool CanWrite { get { return _stream != null && !_isReadOnly; } }

        /// <summary>
        /// Reads a buffer worth of bytes from the stream.
        /// </summary>
        /// <param name="buffer">Buffer to fill</param>
        /// <param name="offset">Offset to start filling in the buffer</param>
        /// <param name="count">Number of bytes to read from the stream</param>
        /// <returns></returns>
        public override int Read(byte[] buffer, int offset, int count) {
            ThrowIfDisposed();

            if (buffer == null) { throw new ArgumentNullException("buffer"); }
            if (offset < 0) { throw new ArgumentOutOfRangeException("offset", "less than 0"); }
            if (count < 0) { throw new ArgumentOutOfRangeException("count", "less than 0"); }
            if (offset + count > buffer.Length) { throw new ArgumentException("overflow", "count"); }

            int bytesRead = 0;
            if (count > 0) {
                IntPtr ptr = Marshal.AllocCoTaskMem(sizeof(ulong));
                try {
                    if (offset == 0) {
                        _stream.Read(buffer, count, ptr);
                        bytesRead = (int)Marshal.ReadInt64(ptr);
                    } else {
                        byte[] tempBuffer = new byte[count];
                        _stream.Read(tempBuffer, count, ptr);

                        bytesRead = (int)Marshal.ReadInt64(ptr);
                        if (bytesRead > 0) {
                            Array.Copy(tempBuffer, 0, buffer, offset, bytesRead);
                        }
                    }
                } finally {
                    Marshal.FreeCoTaskMem(ptr);
                }
            }
            return bytesRead;
        }

        /// <summary>
        /// Writes a buffer to the stream if able to do so.
        /// </summary>
        /// <param name="buffer">Buffer to write</param>
        /// <param name="offset">Offset in buffer to start writing</param>
        /// <param name="count">Number of bytes to write to the stream</param>
        public override void Write(byte[] buffer, int offset, int count) {
            ThrowIfDisposed();

            if (_isReadOnly) { throw new InvalidOperationException("readonly"); }
            if (buffer == null) { throw new ArgumentNullException("buffer"); }
            if (offset < 0) { throw new ArgumentOutOfRangeException("offset", "less than 0"); }
            if (count < 0) { throw new ArgumentOutOfRangeException("count", "less than 0"); }
            if (offset + count > buffer.Length) { throw new ArgumentException("overflow", "count"); }

            if (count > 0) {
                IntPtr ptr = Marshal.AllocCoTaskMem(sizeof(ulong));
                try {
                    if (offset == 0) {
                        _stream.Write(buffer, count, ptr);
                    } else {
                        byte[] tempBuffer = new byte[count];
                        Array.Copy(buffer, offset, tempBuffer, 0, count);
                        _stream.Write(tempBuffer, count, ptr);
                    }
                } finally {
                    Marshal.FreeCoTaskMem(ptr);
                }
            }
        }

        /// <summary>
        /// Gets the length of the IStream
        /// </summary>
        public override long Length {
            get {
                ThrowIfDisposed();
                return _stats.cbSize;
            }
        }

        /// <summary>
        /// Gets the filename of the IStream
        /// </summary>
        public string Filename {
            get {
                ThrowIfDisposed();
                return _stats.pwcsName;
            }
        }

        /// <summary>
        /// Gets the type of the IStream
        /// </summary>
        public int Type {
            get {
                ThrowIfDisposed();
                return _stats.type;
            }
        }

        /// <summary>
        /// Gets or sets the current position within the underlying IStream.
        /// </summary>
        public override long Position {
            get {
                ThrowIfDisposed();
                return Seek(0, SeekOrigin.Current);
            }
            set {
                ThrowIfDisposed();
                Seek(value, SeekOrigin.Begin);
            }
        }

        /// <summary>
        /// Seeks within the underlying IStream.
        /// </summary>
        /// <param name="offset">Offset</param>
        /// <param name="origin">Where to start seeking</param>
        /// <returns></returns>
        public override long Seek(long offset, SeekOrigin origin) {
            ThrowIfDisposed();
            IntPtr ptr = Marshal.AllocCoTaskMem(sizeof(long));
            try {
                _stream.Seek(offset, (int)origin, ptr);
                return Marshal.ReadInt64(ptr);
            } finally {
                Marshal.FreeCoTaskMem(ptr);
            }
        }

        /// <summary>
        /// Sets the length of the stream
        /// </summary>
        /// <param name="value"></param>
        public override void SetLength(long value) {
            ThrowIfDisposed();
            _stream.SetSize(value);
        }

        /// <summary>
        /// Commits data to be written to the stream if it is being cached.
        /// </summary>
        public override void Flush() {
            _stream.Commit((int)StorageStreamCommitOptions.None);
        }

        /// <summary>
        /// Disposes the stream.
        /// </summary>
        /// <param name="disposing">True if called from Dispose(), false if called from finalizer.</param>
        protected override void Dispose(bool disposing) {
            _stream = null;
            base.Dispose(disposing);
        }

        private void ThrowIfDisposed() { if (_stream == null) throw new ObjectDisposedException(GetType().Name); }
    }

    /// <summary>
    /// Options for commiting (flushing) an IStream storage stream
    /// </summary>
    [Flags]
    internal enum StorageStreamCommitOptions {
        /// <summary>
        /// Uses default options
        /// </summary>
        None = 0,
        /// <summary>
        /// Overwrite option
        /// </summary>
        Overwrite = 1,
        /// <summary>
        /// Only if current
        /// </summary>
        OnlyIfCurrent = 2,
        /// <summary>
        /// Commits to disk cache dangerously
        /// </summary>
        DangerouslyCommitMerelyToDiskCache = 4,
        /// <summary>
        /// Consolidate
        /// </summary>
        Consolidate = 8
    }

    #endregion

    #region - Enums -

    /// <summary>
    /// The STGM constants are flags that indicate 
    /// conditions for creating and deleting the object and access modes 
    /// for the object. 
    /// 
    /// You can combine these flags, but you can only choose one flag 
    /// from each group of related flags. Typically one flag from each 
    /// of the access and sharing groups must be specified for all 
    /// functions and methods which use these constants. 
    /// </summary>
    [Flags]
    [SuppressMessage("Microsoft.Design", "CA1008:EnumsShouldHaveZeroValue", Justification = "Follows native api.")]
    enum AccessModes {
        /// <summary>
        /// Indicates that, in direct mode, each change to a storage 
        /// or stream element is written as it occurs.
        /// </summary>
        Direct = 0x00000000,
        /// <summary>
        /// Indicates that, in transacted mode, changes are buffered 
        /// and written only if an explicit commit operation is called. 
        /// </summary>
        Transacted = 0x00010000,
        /// <summary>
        /// Provides a faster implementation of a compound file 
        /// in a limited, but frequently used, case. 
        /// </summary>
        Simple = 0x08000000,
        /// <summary>
        /// Indicates that the object is read-only, 
        /// meaning that modifications cannot be made.
        /// </summary>
        Read = 0x00000000,
        /// <summary>
        /// Enables you to save changes to the object, 
        /// but does not permit access to its data. 
        /// </summary>
        Write = 0x00000001,
        /// <summary>
        /// Enables access and modification of object data.
        /// </summary>
        ReadWrite = 0x00000002,
        /// <summary>
        /// Specifies that subsequent openings of the object are 
        /// not denied read or write access. 
        /// </summary>
        ShareDenyNone = 0x00000040,
        /// <summary>
        /// Prevents others from subsequently opening the object in Read mode. 
        /// </summary>
        ShareDenyRead = 0x00000030,
        /// <summary>
        /// Prevents others from subsequently opening the object 
        /// for Write or ReadWrite access.
        /// </summary>
        ShareDenyWrite = 0x00000020,
        /// <summary>
        /// Prevents others from subsequently opening the object in any mode. 
        /// </summary>
        ShareExclusive = 0x00000010,
        /// <summary>
        /// Opens the storage object with exclusive access to the most 
        /// recently committed version.
        /// </summary>
        Priority = 0x00040000,
        /// <summary>
        /// Indicates that the underlying file is to be automatically destroyed when the root 
        /// storage object is released. This feature is most useful for creating temporary files. 
        /// </summary>
        DeleteOnRelease = 0x04000000,
        /// <summary>
        /// Indicates that, in transacted mode, a temporary scratch file is usually used 
        /// to save modifications until the Commit method is called. 
        /// Specifying NoScratch permits the unused portion of the original file 
        /// to be used as work space instead of creating a new file for that purpose. 
        /// </summary>
        NoScratch = 0x00100000,
        /// <summary>
        /// Indicates that an existing storage object 
        /// or stream should be removed before the new object replaces it. 
        /// </summary>
        Create = 0x00001000,
        /// <summary>
        /// Creates the new object while preserving existing data in a stream named "Contents". 
        /// </summary>
        Convert = 0x00020000,
        /// <summary>
        /// Causes the create operation to fail if an existing object with the specified name exists.
        /// </summary>
        FailIfThere = 0x00000000,
        /// <summary>
        /// This flag is used when opening a storage object with Transacted 
        /// and without ShareExclusive or ShareDenyWrite. 
        /// In this case, specifying NoSnapshot prevents the system-provided 
        /// implementation from creating a snapshot copy of the file. 
        /// Instead, changes to the file are written to the end of the file. 
        /// </summary>
        NoSnapshot = 0x00200000,
        /// <summary>
        /// Supports direct mode for single-writer, multireader file operations. 
        /// </summary>
        DirectSingleWriterMultipleReader = 0x00400000
    }

    #endregion

    #region - Interface -

    /// <summary>
    /// Provides means by which to initialize with a stream.
    /// </summary>
    [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")]
    [ComImport]
    [Guid("b824b49d-22ac-4161-ac8a-9916e8fa3f7f")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    interface IInitializeWithStream {
        /// <summary>
        /// Initializes with a stream.
        /// </summary>
        /// <param name="stream"></param>
        /// <param name="fileMode"></param>
        void Initialize(IStream stream, AccessModes fileMode);
    }

    /// <summary>
    /// Provides means by which to initialize with a file.
    /// </summary>
    [ComImport]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [Guid("b7d14566-0509-4cce-a71f-0a554233bd9b")]
    interface IInitializeWithFile {
        /// <summary>
        /// Initializes with a file.
        /// </summary>
        /// <param name="filePath"></param>
        /// <param name="fileMode"></param>
        void Initialize([MarshalAs(UnmanagedType.LPWStr)] string filePath, AccessModes fileMode);
    }

    #endregion

    #region - NativeMethod -

    static class Native {

        #region - APIs -

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        public static extern void OutputDebugString(string s);
        [DllImport("Shell32.dll")]
        public static extern void SHChangeNotify(int wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
        public const int SHCNE_ASSOCCHANGED = 0x08000000;

        #endregion

        #region - Debug Method -

        public static void OutputDebugString(string format, object arg1) {
            OutputDebugString(format, new object[] { arg1 });
        }
        public static void OutputDebugString(string format, object arg1, object arg2) {
            OutputDebugString(format, new object[] { arg1, arg2 });
        }
        public static void OutputDebugString(string format, object arg1, object arg2, object arg3) {
            OutputDebugString(format, new object[] { arg1, arg2, arg3 });
        }
        public static void OutputDebugString(string format, params object[] args) {
            OutputDebugString(string.Format(format, args));
        }

        #endregion
    }

    #endregion

}
